package org.jfonia.musicxml;

import java.util.ArrayList;

import org.jfonia.harmony.ChordParser;
import org.jfonia.harmony.ChordType;
import org.jfonia.musicxml.model.Degree;
import org.jfonia.musicxml.model.Harmony;
import org.jfonia.musicxml.model.PitchClass;
import org.jfonia.util.JFoniaException;
import org.jfonia.util.Logger;
import org.w3c.dom.Node;

/**
 * creation: 13-nov-2007
 */

/**
 * Class used to parse harmony-info from a string,
 * such as "A7(#5)/C#".
 * <p>
 * Uses ChordParser to parse the ChordType. 
 * This class adds functionality to convert to MusicXML harmony.
 * 
 * @author Wijnand
 *
 */
public class HarmonyParser
{
	private ChordParser chordParser;
	private Logger<Throwable> logger;
	
	private Node node;
	
	private String code;
	private int i;	// index of current char
	private char c; // current char or (char)0 if beyond end of string
	
	
	public HarmonyParser(ChordParser chordParser, Logger<Throwable> logger)
	{
		this.chordParser = chordParser;
		this.logger = logger;
	}
	
	/**
	 * Tries to parse a string (e.g."Fm(b5)") to a Harmony.
	 *  (a.o. things to fix for Dolet bug)
	 *   <p>
	 *   limitations: only one alteration, no bass-pitch
	 *   parameter direction used for exceptions
	 * @param code
	 * @param directionNode
	 * @param partContext
	 * @return harmony or null if format-fault.
	 */
	// 
	public Harmony parse(String code, Node node)  
	{
		if (code==null || code.length()==0)
			return null;
		
//		this.errorHandler = errorHandler;
		this.node = node;
		
		this.code = code;
		this.i = -1;
		this.c = (char)0;
		
		try {
			ArrayList<Degree> altList = new ArrayList<Degree>();
			
			readChar();
			
			PitchClass rootPitchClass = readPitchClass();
			if (rootPitchClass == null)
				return null;
			
			String kind = readUntil("(+/");	
				// TODO: special cases: 7+ can be major seventh
				//                      - can be minor
			
			// first check without alterations:
			try {
				ChordType ct = chordParser.determineChordType(kind);
				if (ct == null)	// no chord
					return null;	
			}
			catch (JFoniaException e)
			{
				return null;	// kind not recognized
			}
			
			if (c=='(')
			{
				readChar();
				
				Degree alt = readAlteration(node, "alter");  // TODO: always "alter"??
				if (alt == null)
					return null;
				
				if (c != ')') 
				{
					logger.log(new CodedMusicXMLImportException("HARMONY_PARSE_CLOSE_EXPECTED", code, node));
					return null;
				}
				readChar();
			
				altList.add(alt); 	
				
			}
			else if (c=='+')
			{
				readChar();
				
				Degree alt = readAlteration(node, "add"); // TODO: alter should be 0
				
				if (alt != null)
					altList.add(alt); // ??	
			}
			
			// this time with alterations:
			ChordType chordType;
			try {
				chordType = determineType(kind, altList);
			}
			catch(MusicXMLImportException e)
			{
				e.setNode(node);
				logger.log(e);
				return null;
			}
			
			PitchClass bassPitchClass = null;
			if (c == '/')
			{
				readChar();
				
				bassPitchClass = readPitchClass();
			}
	
			if (c != (char)0)
			{
				logger.log(new CodedMusicXMLImportException("HARMONY_PARSE_CHAR_UNEXPECTED", code, node));
				return null;
			}

			Harmony h = new Harmony(null, rootPitchClass, bassPitchClass, -1, chordType);
			return h;

		}
		catch(IndexOutOfBoundsException e)
		{
			return null;
		}
		
	}


	/**
	 * 
	 * @param kind
	 * @param alterationList
	 * @return  or null if "no chord"
	 * @throws MusicXMLException if not recognized
	 */
	public ChordType determineType(String kind, ArrayList<Degree> alterationList)
		throws CodedMusicXMLImportException
	{
		ChordType ct;
		try {
			ct = chordParser.determineChordType(kind);
		}
		catch (JFoniaException e)
		{
			return null;
		}
		if (ct == null)	// no chord
			return null;
		
		for (Degree alt: alterationList)
		{
			int d = alt.getDegreeValue();
			int a = alt.getDegreeAlter();
			// TODO: in case of subtract/alter: degree alteration is relative to the degree in kind
			// e.g. if kind is dom7 then a==-1 means make 7th double flat!!
			String t = alt.getDegreeType();
			if (t.equals("subtract"))
				ct.setType(d, ChordType.OMIT);
			else if (t.equals("add"))
			{
				if (d==7)
					ct.setType(d, a-1);
				else
					ct.setType(d, a);	// assert(a==0) ??
			}
			else if (t.equals("alter"))
				ct.setType(d, a);
			else
				throw new CodedMusicXMLImportException("ALT_DEGREE_TYPE_UNKNOWN", t, null);
		}
		
		return ct;
	}

//	static char getChar(String code, int i)
//	{
//		return i<code.length() ? code.charAt(i) : (char)0;
//	}

	private void readChar()
	{
		i++;
		c = i<code.length() ? code.charAt(i) : (char)0;
	}
	
	private PitchClass readPitchClass()
	{
		String step;
		int alter = 0;

		if ("ABCDEFG".indexOf(c) < 0)
			return null;
		step = String.valueOf(c);
		readChar();
		
		if (c=='b' || c=='#')
		{
			alter = (c=='b' ? -1 : 1);	
			readChar();
		}

		return new PitchClass(null, step, alter);
	}
	
	private String readUntil(String endChars)
	{
		StringBuffer kindBuf = new StringBuffer();
		while (c!=(char)0 && endChars.indexOf(c) == -1)
		{
			kindBuf.append(c);
			readChar();
		}
		return kindBuf.toString();
	}
	
	
	private Degree readAlteration(Node node, String alterType)
	{
		int altAlter = 0;
		int altValue;
		
		if (c=='b')
		{
			altAlter = -1;
			readChar();
		}
		else if (c=='#')
		{
			altAlter = 1;
			readChar();
		}
		else 
		{
			logger.log(new CodedMusicXMLImportException("HARMONY_PARSE_ALT_UNKNOWN", code , node));
			return null;
		}
		
		if (Character.isDigit(c))
		{
			altValue = Character.digit(c,10);
			readChar();
		}
		else
		{
			logger.log(new CodedMusicXMLImportException("HARMONY_PARSE_DIGIT_EXPECTED", code , node));
			return null;
		}
		if (Character.isDigit(c))	// second digit?
		{
			altValue = altValue*10 + Character.digit(c, 10);
			readChar();
		}

		return new Degree(node, altValue, altAlter, alterType);
	}

	
	public static void main(String[] args)
	{
		try {
			ChordParser cp = new ChordParser("org.jfonia.musicxml.model.musicxmlchordtypes");
		
			
			HarmonyParser hp = new HarmonyParser(cp, new Logger<Throwable>());
			
			
			String[] testcodes = { "C", "Dm", "Em7", "F#", "G#m", "A#m7", "Bb", "Gbm", "G(#5)", "Gm7(b5)", "G7(#11)",
					               "H", "A/G", "Am/E",
					               "G&", "", "G(b5", "G(b5)G", "G(b)", "G()", "G(ba)", "G(d5)",
					               "A6", "F#6", "Am6", "Am7", };
			for (String code: testcodes)
			{
				System.out.print(code + " : ");
				Harmony h = hp.parse(code, null);
				System.out.println( h==null ? "------" : h.toString() );
				hp.getLogger().report();
			}
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
	}

	public Logger<Throwable> getLogger()
	{
		return logger;
	}

	public void setLogger(Logger<Throwable> logger)
	{
		this.logger = logger;
	}

}
